/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.felix.framework.util;

import com.quickfile.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import com.quickfile.util.ZipFile;

/**
 * This class implements a factory for creating weak zip files, which behave
 * mostly like a ZipFile, but can be weakly closed to limit the number of open
 * files.
 */
public class WeakZipFileFactory {
	private static final int WEAKLY_CLOSED = 0;
	private static final int OPEN = 1;
	private static final int CLOSED = 2;

	private static final SecureAction m_secureAction = new SecureAction();

	private final List<WeakZipFile> m_zipFiles = new ArrayList<WeakZipFile>();
	private final List<WeakZipFile> m_openFiles = new ArrayList<WeakZipFile>();
	private final Mutex m_globalMutex = new Mutex();
	private final int m_limit;

	/**
	 * Constructs a weak zip file factory with the specified file limit. A limit
	 * of zero signifies no limit.
	 * 
	 * @param limit
	 *            maximum number of open zip files at any given time.
	 */
	public WeakZipFileFactory(int limit) {
		if (limit < 0) {
			throw new IllegalArgumentException("Limit must be non-negative.");
		}
		m_limit = limit;
	}

	/**
	 * Factory method used to create weak zip files.
	 * 
	 * @param file
	 *            the target zip file.
	 * @return the created weak zip file.
	 * @throws IOException
	 *             if the zip file could not be opened.
	 */
	public WeakZipFile create(File file) throws IOException {
		WeakZipFile wzf = new WeakZipFile(file);

		if (m_limit > 0) {
			try {
				m_globalMutex.down();
			} catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
				throw new IOException(
						"Interrupted while acquiring global zip file mutex.");
			}

			try {
				m_zipFiles.add(wzf);
				m_openFiles.add(wzf);
				if (m_openFiles.size() > m_limit) {
					WeakZipFile candidate = m_openFiles.get(0);
					for (WeakZipFile tmp : m_openFiles) {
						if (candidate.m_timestamp > tmp.m_timestamp) {
							candidate = tmp;
						}
					}
					candidate._closeWeakly();
				}
			} finally {
				m_globalMutex.up();
			}
		}

		return wzf;
	}

	/**
	 * Only used for testing.
	 * 
	 * @return unclosed weak zip files.
	 **/
	List<WeakZipFile> getZipZiles() {
		try {
			m_globalMutex.down();
		} catch (InterruptedException ex) {
			Thread.currentThread().interrupt();
			return Collections.EMPTY_LIST;
		}

		try {
			return m_zipFiles;
		} finally {
			m_globalMutex.up();
		}
	}

	/**
	 * Only used for testing.
	 * 
	 * @return open weak zip files.
	 **/
	List<WeakZipFile> getOpenZipZiles() {
		try {
			m_globalMutex.down();
		} catch (InterruptedException ex) {
			Thread.currentThread().interrupt();
			return Collections.EMPTY_LIST;
		}

		try {
			return m_openFiles;
		} finally {
			m_globalMutex.up();
		}
	}

	/**
	 * This class wraps a ZipFile to making it possible to weakly close it; this
	 * means the underlying zip file will be automatically reopened on demand if
	 * anyone tries to use it.
	 */
	public class WeakZipFile {
		private final File m_file;
		private final Mutex m_localMutex = new Mutex();
		private ZipFile m_zipFile;
		private int m_status = OPEN;
		private long m_timestamp;

		/**
		 * Constructor is private since instances need to be centrally managed.
		 * 
		 * @param file
		 *            the target zip file.
		 * @throws IOException
		 *             if the zip file could not be opened.
		 */
		private WeakZipFile(File file) throws IOException {
			m_file = file;
			m_zipFile = m_secureAction.openZipFile(m_file);
			m_timestamp = System.currentTimeMillis();
		}

		/**
		 * Returns the specified entry from the zip file.
		 * 
		 * @param name
		 *            the name of the entry to return.
		 * @return the zip entry associated with the specified name or null if
		 *         it does not exist.
		 */
		public ZipEntry getEntry(String name) {
			ensureZipFileIsOpen();

			try {
				ZipEntry ze = null;
				ze = m_zipFile.getEntry(name);
				if ((ze != null) && (ze.getSize() == 0) && !ze.isDirectory()) {
					// The attempts to fix an apparent bug in the JVM in
					// versions
					// 1.4.2 and lower where directory entries in ZIP/JAR files
					// are
					// not correctly identified.
					ZipEntry dirEntry = m_zipFile.getEntry(name + '/');
					if (dirEntry != null) {
						ze = dirEntry;
					}
				}
				return ze;
			} catch (Exception ex) {
				throw new RuntimeException(ex);
			} finally {
				m_localMutex.up();
			}
		}

		/**
		 * Returns an enumeration of zip entries from the zip file.
		 * 
		 * @return an enumeration of zip entries.
		 */
		public Enumeration<ZipEntry> entries() {
			ensureZipFileIsOpen();

			try {
				// We need to suck in all of the entries since the zip
				// file may get weakly closed during iteration. Technically,
				// this may not be 100% correct either since if the zip file
				// gets weakly closed and reopened, then the zip entries
				// will be from a different zip file. It is not clear if this
				// will cause any issues.
				Enumeration<? extends ZipEntry> e = m_zipFile.entries();
				List<ZipEntry> entries = new ArrayList<ZipEntry>();
				while (e.hasMoreElements()) {
					entries.add(e.nextElement());
				}
				return Collections.enumeration(entries);
			} catch (Exception ex) {
				throw new RuntimeException(ex);
			} finally {
				m_localMutex.up();
			}
		}

		/**
		 * Returns an input stream for the specified zip entry.
		 * 
		 * @param ze
		 *            the zip entry whose input stream is to be retrieved.
		 * @return an input stream to the zip entry.
		 * @throws IOException
		 *             if the input stream cannot be opened.
		 */
		public InputStream getInputStream(ZipEntry ze) throws IOException {
			ensureZipFileIsOpen();

			try {
				InputStream is = m_zipFile.getInputStream(ze);
				return new WeakZipInputStream(ze.getName(), is);
			} finally {
				m_localMutex.up();
			}
		}

		/**
		 * Weakly closes the zip file, which means that it will be reopened if
		 * anyone tries to use it again.
		 */
		void closeWeakly() {
			try {
				m_globalMutex.down();
			} catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
				throw new IllegalStateException(
						"Interrupted while acquiring global zip file mutex.");
			}

			try {
				_closeWeakly();
			} finally {
				m_globalMutex.up();
			}
		}

		/**
		 * This method is used internally to weakly close a zip file. It should
		 * only be called when already holding the global lock, otherwise use
		 * closeWeakly().
		 */
		private void _closeWeakly() {
			try {
				m_localMutex.down();
			} catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
				throw new IllegalStateException(
						"Interrupted while acquiring local zip file mutex.");
			}

			try {
				if (m_status == OPEN) {
					try {
						m_status = WEAKLY_CLOSED;
						if (m_zipFile != null) {
							m_zipFile.close();
							m_zipFile = null;
						}
						m_openFiles.remove(this);
					} catch (IOException ex) {
						__close();
					}
				}
			} finally {
				m_localMutex.up();
			}
		}

		/**
		 * This method permanently closes the zip file.
		 * 
		 * @throws IOException
		 *             if any error occurs while trying to close the zip file.
		 */
		public void close() throws IOException {
			if (m_limit > 0) {
				try {
					m_globalMutex.down();
				} catch (InterruptedException ex) {
					Thread.currentThread().interrupt();
					throw new IllegalStateException(
							"Interrupted while acquiring global zip file mutex.");
				}
				try {
					m_localMutex.down();
				} catch (InterruptedException ex) {
					m_globalMutex.up();
					Thread.currentThread().interrupt();
					throw new IllegalStateException(
							"Interrupted while acquiring local zip file mutex.");
				}
			}

			try {
				ZipFile tmp = m_zipFile;
				__close();
				if (tmp != null) {
					tmp.close();
				}
			} finally {
				m_localMutex.up();
				m_globalMutex.up();
			}
		}

		/**
		 * This internal method is used to clear the zip file from the data
		 * structures and reset its state. It should only be called when holding
		 * the global and local mutexes.
		 */
		private void __close() {
			m_status = CLOSED;
			m_zipFile = null;
			m_zipFiles.remove(this);
			m_openFiles.remove(this);
		}

		/**
		 * This method ensures that the zip file associated with this weak zip
		 * file instance is actually open and acquires the local weak zip file
		 * mutex. If the underlying zip file is closed, then the local mutex is
		 * released and an IllegalStateException is thrown. If the zip file is
		 * weakly closed, then it is reopened. If the zip file is already
		 * opened, then no additional action is necessary. If this method does
		 * not throw an exception, then the end result is the zip file member
		 * field is non-null and the local mutex has been acquired.
		 */
		private void ensureZipFileIsOpen() {
			if (m_limit == 0) {
				return;
			}

			// Get mutex for zip file.
			try {
				m_localMutex.down();
			} catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
				throw new IllegalStateException(
						"Interrupted while acquiring local zip file mutex.");
			}

			// If zip file is closed, then just return null.
			if (m_status == CLOSED) {
				m_localMutex.up();
				throw new IllegalStateException("Zip file is closed: " + m_file);
			}

			// If zip file is weakly closed, we need to reopen it,
			// but we have to release the zip mutex to acquire the
			// global mutex, then reacquire the zip mutex. This
			// ensures that the global mutex is always acquired
			// before any local mutex to avoid deadlocks.
			IOException cause = null;
			if (m_status == WEAKLY_CLOSED) {
				m_localMutex.up();

				try {
					m_globalMutex.down();
				} catch (InterruptedException ex) {
					Thread.currentThread().interrupt();
					throw new IllegalStateException(
							"Interrupted while acquiring global zip file mutex.");
				}
				try {
					m_localMutex.down();
				} catch (InterruptedException ex) {
					m_globalMutex.up();
					Thread.currentThread().interrupt();
					throw new IllegalStateException(
							"Interrupted while acquiring local zip file mutex.");
				}

				// Double check status since it may have changed.
				if (m_status == CLOSED) {
					m_localMutex.up();
					m_globalMutex.up();
					throw new IllegalStateException("Zip file is closed: "
							+ m_file);
				} else if (m_status == WEAKLY_CLOSED) {
					try {
						__reopenZipFile();
					} catch (IOException ex) {
						cause = ex;
					}
				}

				// Release the global mutex, since it should no longer be
				// necessary.
				m_globalMutex.up();
			}

			// It is possible that reopening the zip file failed, so we check
			// for that case and throw an exception.
			if (m_zipFile == null) {
				m_localMutex.up();
				IllegalStateException ise = new IllegalStateException(
						"Zip file is closed: " + m_file);
				if (cause != null) {
					ise.initCause(cause);
				}
				throw ise;
			}
		}

		/**
		 * Thie internal method is used to reopen a weakly closed zip file. It
		 * makes a best effort, but may fail and leave the zip file member field
		 * null. Any failure reopening a zip file results in it being
		 * permanently closed. This method should only be invoked when holding
		 * the global and local mutexes.
		 */
		private void __reopenZipFile() throws IOException {
			if (m_status == WEAKLY_CLOSED) {
				try {
					m_zipFile = m_secureAction.openZipFile(m_file);
					m_status = OPEN;
					m_timestamp = System.currentTimeMillis();
				} catch (IOException ex) {
					__close();
					throw ex;
				}

				if (m_zipFile != null) {
					m_openFiles.add(this);
					if (m_openFiles.size() > m_limit) {
						WeakZipFile candidate = m_openFiles.get(0);
						for (WeakZipFile tmp : m_openFiles) {
							if (candidate.m_timestamp > tmp.m_timestamp) {
								candidate = tmp;
							}
						}
						candidate._closeWeakly();
					}
				}
			}
		}

		/**
		 * This is an InputStream wrapper that will properly reopen the
		 * underlying zip file if it is weakly closed and create the underlying
		 * input stream.
		 */
		class WeakZipInputStream extends InputStream {
			private final String m_entryName;
			private InputStream m_is;
			private int m_currentPos = 0;
			private ZipFile m_zipFileSnapshot;

			WeakZipInputStream(String entryName, InputStream is) {
				m_entryName = entryName;
				m_is = is;
				m_zipFileSnapshot = m_zipFile;
			}

			/**
			 * This internal method ensures that the zip file is open and that
			 * the underlying input stream is valid. Upon successful completion,
			 * the underlying input stream will be valid and the local mutex
			 * will be held.
			 * 
			 * @throws IOException
			 *             if the was an error handling the input stream.
			 */
			private void ensureInputStreamIsValid() throws IOException {
				if (m_limit == 0) {
					return;
				}

				ensureZipFileIsOpen();

				// If the underlying zip file changed, then we need
				// to get the input stream again.
				if (m_zipFileSnapshot != m_zipFile) {
					m_zipFileSnapshot = m_zipFile;

					if (m_is != null) {
						try {
							m_is.close();
						} catch (Exception ex) {
							// Not much we can do.
						}
					}
					try {
						m_is = m_zipFile.getInputStream(m_zipFile
								.getEntry(m_entryName));
						m_is.skip(m_currentPos);
					} catch (IOException ex) {
						m_localMutex.up();
						throw ex;
					}
				}
			}

			@Override
			public int available() throws IOException {
				ensureInputStreamIsValid();
				try {
					return m_is.available();
				} finally {
					m_localMutex.up();
				}
			}

			@Override
			public void close() throws IOException {
				ensureInputStreamIsValid();
				try {
					InputStream is = m_is;
					m_is = null;
					if (is != null) {
						is.close();
					}
				} finally {
					m_localMutex.up();
				}
			}

			@Override
			public void mark(int i) {
				// Not supported.
			}

			@Override
			public boolean markSupported() {
				// Not supported.
				return false;
			}

			public int read() throws IOException {
				ensureInputStreamIsValid();
				try {
					int len = m_is.read();
					if (len > 0) {
						m_currentPos++;
					}
					return len;
				} finally {
					m_localMutex.up();
				}
			}

			@Override
			public int read(byte[] bytes) throws IOException {
				ensureInputStreamIsValid();
				try {
					int len = m_is.read(bytes);
					if (len > 0) {
						m_currentPos += len;
					}
					return len;
				} finally {
					m_localMutex.up();
				}
			}

			@Override
			public int read(byte[] bytes, int i, int i1) throws IOException {
				ensureInputStreamIsValid();
				try {
					int len = m_is.read(bytes, i, i1);
					if (len > 0) {
						m_currentPos += len;
					}
					return len;
				} finally {
					m_localMutex.up();
				}
			}

			@Override
			public void reset() throws IOException {
				throw new IOException("Unsupported operation");
			}

			@Override
			public long skip(long l) throws IOException {
				ensureInputStreamIsValid();
				try {
					long len = m_is.skip(l);
					if (len > 0) {
						m_currentPos += len;
					}
					return len;
				} finally {
					m_localMutex.up();
				}
			}
		}
	}
}
